AWS CDK で CloudWatch Synthetics Canary を設定する方法
Introduction
様々な外形監視のパターンが実現できる CloudWatch Synthetics Canary は今年 4月末に GA になっていたんですが、GUI 上で一個ずつ設定するのに結構時間かかるし、Lambda のコードもテキストファイルに書くほどの壁など色々扱いにくかったので、実際に本番環境で運用しているプロダクションは結構少ないと思われます。そして、Terraform もまだサポートしていない (*1) ことを見つけてちょっとコード化は諦めていたんですが、なんと 5日前に CDK サポートリリースが出てきたので早速試した経験を共有します。
*1) https://github.com/terraform-providers/terraform-provider-aws/pull/13140
プレスリリース
必須条件
- AWS CDK
- v1.59.0 or later
関連プルリクエスト
https://github.com/aws/aws-cdk/pull/8824
Goal
- Github ページを 10分間隔で外形監視
- 連続で 2回失敗したらアラート
- Github API を 3分間隔で外形監視
- 1回失敗したらアラート
登場人物
- CloudWatch
- Synthetics Canary
- Alarm
Getting Started
- バージョン
- Node.js: v12.12.0
- TypeScript: v3.7.5
- AWS CDK: v1.59.0
管理画面向けの外形監視コードを実装
var synthetics = require('Synthetics'); const log = require('SyntheticsLogger'); const screenCanary = async function () { const URL = "https://github.com/sano307"; try { let page = await synthetics.getPage(); const response = await page.goto(URL, {waitUntil: 'networkidle2', timeout: 0}); if (!response || response.status() !== 200) { throw "Failed to load page!"; } await page.waitFor(5000); await synthetics.takeScreenshot('loaded', 'loaded'); let pageTitle = await page.title(); log.info('Page title: ' + pageTitle); } catch (e) { console.log(e); throw e; } }; exports.handler = async () => { return await screenCanary(); };
- まず、
Puppeteer
をラッパーしたSynthetics
パッケージと Synthetics ログ専用のSyntheticsLogger
パッケージが必要です。こちらのパッケージはpackage.json
に追加する必要なく AWS 上にデプロイされると勝手に参照できるようになるので気にしなくても大丈夫です。 - 続いて Github ページの情報を取得する必要があるんですが、そこに page.goto() が使えます。ページの取得判定基準として様々な HTML Window event が選べるんですが、今回はちょっとゆるくするために
networkidle2
を設定しました。そして、timeout を 0 に設定しているんですが、timeout を設定して置くと、たまに Navigation Timeout が出てくる障害が起きたので、timeout なしにしました。 page.waitFor()
でページの情報が完全に取得できるまで待っていたり、synthetics.takeScreenshot()
でページの画面を撮ったりなどPuppeteer
の API で可能な挙動は問題なく追加できるので、外形監視の自由度がかなり高くなった気がします。- Canary の Lambda ファイルの経路にはちょっとこだわりがありまして、必ず
nodejs/node_modules
下に Lambda ファイルを配置しなければならないです。おそらく該当する経路にある Lambda ファイルは Canary 向けの Lambda だと判定しSynthetics
関連のパッケージが勝手に紐付けられる仕様になっているんじゃないかと思っています。 - また、
Synthetics
パッケージが手元にないため TypeScript から JavaScript への変換ができないので、CDK プロジェクトが TypeScript である方は Canary Lambda をコミット対象にする必要があります。
# AWS Synthetics Canary directory !/lambda/canary/nodejs/node_modules/ !/lambda/canary/nodejs/node_modules/*
管理画面向けの外形監視を設定
... import { Canary, Code, Schedule, Test } from '@aws-cdk/aws-synthetics'; import * as path from 'path'; export class SyntheticsCanaryStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); ... const screenCanary = new Canary(this, 'screen-canary', { canaryName: 'screen-canary', schedule: Schedule.rate(cdk.Duration.minutes(10)), test: Test.custom({ code: Code.fromAsset(path.join(__dirname, '../lambda/canary')), handler: 'screen-canary.handler' }) }) } }
- 適当にリソース名を設定し、外形監視の間隔を 10分に設定しましょう。
- あと、Canary Lambda ファイルと紐づく必要があるんですが、
nodejs/node_modules
の上位経路まで設定する必要があります。今回は../lambda/canary/nodejs/node_modules
なので../lambda/canary
まで設定して置けば大丈夫です。
管理画面向けの外形監視にアラームを設定
... import { Alarm, ComparisonOperator } from '@aws-cdk/aws-cloudwatch'; export class SyntheticsCanaryStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); ... new Alarm(this, 'screen-canary-alarm', { alarmName: 'screen-canary-alarm', metric: screenCanary.metricSuccessPercent(), statistic: "SampleCount", threshold: 1, comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD, period: cdk.Duration.minutes(10), evaluationPeriods: 2 }) } }
- 続いてアラートなんですが、条件通り 2回失敗したら引っかかるように設定しましょう。
- ちなみに
evaluationPeriods
はperiod
で設定した間隔を何回評価 window として扱うかを指定するオプションです。今回だとperiod
が 10分でevaluationPeriods
が 2なので 20分が評価 window になります。
API 向けの外形監視コードを実装
var synthetics = require('Synthetics'); const log = require('SyntheticsLogger'); const https = require('https'); const apiCanary = async function () { const verifyRequest = async function (requestOption) { return new Promise((resolve, reject) => { log.info("Making request with options: " + JSON.stringify(requestOption)); let req = https.request(requestOption); req.on('response', (res) => { log.info(`Status Code: ${res.statusCode}`) log.info(`Response Headers: ${JSON.stringify(res.headers)}`) const responseCode = res.statusCode; if (responseCode !== 200) { reject("Failed: " + requestOption.path); } res.on('data', (d) => { log.info("Response: " + d); }); res.on('end', () => { resolve(); }) }); req.on('error', (error) => { reject(error); }); req.end(); }); } const headers = { "Authorization": "token GITHUB_TOKEN" } headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' '); const requestOptions = { "hostname": "api.github.com", "method": "GET", "path": "/user", "port": 443 } requestOptions['headers'] = headers; await verifyRequest(requestOptions); }; exports.handler = async () => { return await apiCanary(); };
- Github API を叩く必要があるので、
Personal access tokens
を発行する必要があります。トークンの発行ページ (*2) に入ってscope
は repo だけチェックして置きましょう。
*2) Github profile → settings → Developer Settings → Personal access tokens → Generate new token
- 発行したトークンを
GITHUB_TOKEN
に設定してユーザー情報を返してくれるapi.github.com/user
を試してみましょう。
curl -s -H "Authorization: token GITHUB_TOKEN" https://api.github.com | grep current_user_url "current_user_url": "https://api.github.com/user",
API 向けの外形監視を設定
... export class SyntheticsCanaryStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); ... const apiCanary = new Canary(this, 'api-canary', { canaryName: 'api-canary', schedule: Schedule.rate(cdk.Duration.minutes(3)), test: Test.custom({ code: Code.fromAsset(path.join(__dirname, '../lambda/canary')), handler: 'api-canary.handler' }) }) } }
- こちらも適当にリソース名を設定し、外形監視の間隔を 3分に設定しましょう。
API 向けの外形監視にアラームを設定
... export class SyntheticsCanaryStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); ... new Alarm(this, 'api-canary-alarm', { alarmName: 'api-canary-alarm', metric: apiCanary.metricSuccessPercent(), evaluationPeriods: 1, threshold: 1, statistic: "SampleCount", comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD, period: cdk.Duration.minutes(3), }) } }
- API 側のアラートは 1回失敗したら引っかかる条件だったので、評価 window の拡張はしません。なので、
evaluationPeriods
は 1にしています。
Stack 一覧
#!/usr/bin/env node import 'source-map-support/register'; import * as cdk from '@aws-cdk/core'; import { SyntheticsCanaryStack } from '../lib/synthetics-canary-stack'; const app = new cdk.App(); new SyntheticsCanaryStack(app, 'synthetics-canary-stack', { env: { account: 'xxxxxxxxxxxx', region: 'ap-northeast-1' } })
Deploy & Confirm
cdk deploy --require-approval never
Github ページの外形監視
期待通り動いていることが分かります。
synthetics.takeScreenshot()
で撮ったスクリーンショットもちゃんと表示されていますね。加えて JS や CSS などの取得情報は HAR ファイル
から確認できるし、ログも成功・失敗に関係なく自動的に S3 へアップロードされるので、トラブルシューティングが結構楽な気がします。
Github API の外形監視
Bonus
まだ CDK には反映されていない機能なんですが、Canary を特定な VPC 内部で配置するのも可能です。外形監視したいリソースが Inbound IP を制限している場合、この機能を使って同じ Security Group を Canary に設定することで外形監視ができるようになります。
ですが、今回進めている案件の環境で VPC を設定した Canary を試した時に下のエラーが出て来ました。不規則的に Lambda がタイムアウトされることが分かったんですが、原因が特定できず丸 2日が溶けてしまいました。
Kinds | Error |
---|---|
画面系 (Puppeteer 採用) | net::ERR_CONNECTION_TIMED_OUTnet::ERR_TIMED_OUT |
API系 (HTTPS パッケージ採用) | Error: connect ETIMEDOUT |
色々調べた結果、Canary には private subnet
のみ設定しないといけないルール (*3) がありました。僕の設定だと public subnet
と private subnet
が 1つずつ紐づいていたので、おそらく Lambda が public subnet
上で実行されたらインターネットゲートウェイ経由で失敗が発生し、失敗の数がしきい値を超えるとかある時間が経ったら private subnet
に切り替えて NAT ゲートウェイ経由で成功になり、不規則的に成功と失敗が混ぜて起きていたかなと推測しています。
*3) https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_VPC.html の No test result returned" Error
参考
Summary
監視の設定がコードでかけるのはレビューのやりやすさとか監視追加の速さなど色々メリットがあると思っています。ぜひみなさん試してみてください!
この記事が誰かのお役に立てれば幸いです。
以上、CX事業本部 MADチーム、キム (@sano3071) でした。